//
// Created by LiuYou on 2021/5/7.
//

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cassert>
#include <sys/poll.h>
#include <cerrno>
#include <unistd.h>
#include <string>
#include "ClientData.hpp"

using Server07::ClientData;
using String = std::string;

int main(int argc, char** argv) {
    if (argc < 3) {
        std::fprintf(stderr, "用法: %s ip地址 端口号\n", basename(argv[0]));
        return -1;
    }
    const char* const ip = argv[1];
    int port = std::atoi(argv[2]);

    struct sockaddr_in serverSockAddrIn{
            .sin_family=PF_INET,
            .sin_port=htons(port),
            {.s_addr=0}
    };
    inet_pton(AF_INET, ip, &serverSockAddrIn.sin_addr);

    int listenFD = socket(PF_INET, SOCK_STREAM, 0);
    assert(listenFD >= 0);

    int ret = bind(listenFD, (sockaddr*) &serverSockAddrIn, sizeof(serverSockAddrIn));
    assert(ret != -1);

    ret = listen(listenFD, 5);
    assert(ret != -1);

    const int FD_LIMIT = 65535;
    ClientData* clientsData = new ClientData[FD_LIMIT]();
    const int USER_LIMIT = 5;
    pollfd pollFDS[USER_LIMIT + 1];
    for (int i = 1; i < USER_LIMIT; ++i) {
        pollFDS[i].fd = -1;
        pollFDS[i].events = 0;
    }
    // poll轮询的第一个fd是listenFD, poll监听是否有新的client连接。
    pollFDS[0].fd = listenFD;
    // 监听listenFD 是否有新的client连接或者错误
    pollFDS[0].events = POLLIN | POLLERR;
    pollFDS[0].revents = 0;

    int clientsCounter{};
    for (;;) {
        ret = poll(pollFDS, clientsCounter + 1, -1);
        if (ret < 0) {
            std::fprintf(stderr, "poll()调用失败\n");
            break;
        }

        for (int i = 0; i < clientsCounter + 1; ++i) {

            if ((pollFDS[i].fd == listenFD) && (pollFDS[i].revents & POLLIN)) {
                // 如果poll监听的文件是listenFD, 而且listenFD由内核填充的事假发生的事件 有数据可读。(即有新的客户端连接)
                struct sockaddr_in clientSockAddrIn{};
                socklen_t clientSockAddrInLength = sizeof(clientSockAddrIn);
                // 使用新式的C++强转
                int connectionFD = accept(listenFD, reinterpret_cast<sockaddr*>(&clientSockAddrIn),
                                          &clientSockAddrInLength);
                if (connectionFD < 0) {
                    std::fprintf(stderr, "accept(): %s\n", std::strerror(errno));
                    continue;
                }

                // TODO  注意: condition
                if (clientsCounter > USER_LIMIT) {
                    const char* const info = "太多用户连接\n";
                    std::fprintf(stderr, "%s", info);
                    // TODO  注意: send给谁
                    send(connectionFD, info, std::strlen(info), 0);
                    close(connectionFD);
                    continue;
                }

                ++clientsCounter;
                // TODO 注意: 以空间来换取效率。
                clientsData[connectionFD].setSockAddrIn(clientSockAddrIn);

                // TODO 私聊:
                //  先接受一下, 这个连接的Client的发送的用户名
                const int USER_NAME_SIZE = 1024;
                char userName[USER_NAME_SIZE]{};
                recv(connectionFD, userName, USER_NAME_SIZE, 0);
                clientsData[connectionFD].setUserName(userName);
                // 测试一下先
                // 将Client的地址转换为点十进制。
                // TODO 私聊: 这里需要再加一个字段, ip的点十进制字符串表示。
//                inet_ntop(AF_INET, &clientsData[connectionFD].getSockAddrIn().sin_addr,
//                          clientsData[connectionFD].ip, INET_ADDRSTRLEN);
//                std::fprintf(stdout, "该用户为: %s, ip: %s\n",
//                             clientsData[connectionFD].getUserName().data(),
//                             clientsData[connectionFD].ip);


                // 设置为非阻塞, 没有内容(读不到socket内容)我就走了。执行后续的代码。
                Server07::setNonBlocking(connectionFD);
                // 如果是第一次执行到这里, 就是把第一个(连接到Server的Client的Server的)socket文件放到poll监听队列中,
                // 第一次的话就是pollFDS[i].fd = connectionFD; 而pollFDS[0]在监听listenFD。
                // TODO 注意: 上面这行的注释的 i 是 FD_LIMIT的值为 65535 的随便一个, 因为是以空间换取效率。
                //  ulimit -a 查看 为: open files                      (-n) 100001。
                //  这个就是以空间换取效率
                pollFDS[clientsCounter].fd = connectionFD;
                // 注册想要监听的事件: 可读、tcp连接被server关闭、错误
                pollFDS[clientsCounter].events = POLLIN | POLLRDHUP | POLLERR;
                pollFDS[clientsCounter].revents = 0;
                std::fprintf(stdout, "\n连入一个新的用户: %s, 现在有 %d 用户, 分别是:\n",
                             clientsData[connectionFD].getUserName().data(),
                             clientsCounter);
                std::fflush(stdout);
                for (int j = 1; j <= clientsCounter; ++j) {
                    std::fprintf(stdout, "%s:%s\n",
                                 clientsData[pollFDS[j].fd].getIp(),
                                 clientsData[pollFDS[j].fd].getUserName().data());
                }
                std::fflush(stdout);


#ifdef error_1_online
                std::string chatRoomUsers("当前聊天室成员为: ");
                // TODO 私聊:
                //  向每一个人通知某个用户上线信息
                for (int j = 1; j <= clientsCounter; ++j) {

                    /*
                     * TODO 注意: 暂时取消其他客户连接的可读事件, 因为服务器要先发送数据到每一个客户端
                     * 这句代码应该写成 fds[j].events &= ~POLLIN;否则所有选项都将置位，程序虽然能正常运行，但不是作者本意。
                     */
                    // TODO https://bbs.csdn.net/topics/396849194?list=70278186
                    // TODO https://blog.csdn.net/weixin_38514703/article/details/106254010
                    // TODO https://blog.csdn.net/gatieme/article/details/50978320
                    pollFDS[j].events |= ~POLLIN;
                    // 期待数据可写这件事儿
                    pollFDS[j].events |= POLLOUT;
                    // 将当前的connectionFD的内容全部发送给其它连接到Server的Client。

                    // TODO 注意: 这个要放到循环外面
//                    std::string chatRoomUsers("当前聊天室成员为: ");
                    chatRoomUsers += clientsData[pollFDS[j].fd].getUserName();
//                    clientsData[pollFDS[j].fd].setWriteBuffer(const_cast<char*>(chatRoomUsers.data()));

                    // TODO 注意: 这个也是要放在循环外面
                    //  不对, 我要了正确的思路了, 先将信息放在一个循环中, 再将这个循环放在信息循环的下面！
//                    clientsData[pollFDS[j].fd].writeBuffer = clientsData[pollFDS[j].fd].buffer;
//                    std::strcpy(clientsData[pollFDS[j].fd].writeBuffer, chatRoomUsers.data());
                }
//                clientsData[pollFDS[j].fd].writeBuffer = clientsData[pollFDS[j].fd].buffer;
//                std::strcpy(clientsData[pollFDS[j].fd].writeBuffer, chatRoomUsers.data());
#endif

                // TODO 私聊:
                //  向每一个人通知某个用户上线信息
                std::string chatRoomUsers("当前聊天室成员为: ");
                for (int j = 1; j <= clientsCounter; ++j) {
                    chatRoomUsers += clientsData[pollFDS[j].fd].getUserName();
                }
                for (int j = 1; j <= clientsCounter; ++j) {

                    /*
                     * TODO 注意: 暂时取消其他客户连接的可读事件, 因为服务器要先发送数据到每一个客户端
                     * 这句代码应该写成 fds[j].events &= ~POLLIN;否则所有选项都将置位，程序虽然能正常运行，但不是作者本意。
                     */
                    // TODO https://bbs.csdn.net/topics/396849194?list=70278186
                    // TODO https://blog.csdn.net/weixin_38514703/article/details/106254010
                    // TODO https://blog.csdn.net/gatieme/article/details/50978320
                    pollFDS[j].events |= ~POLLIN;
                    // 期待数据可写这件事儿
                    pollFDS[j].events |= POLLOUT;
                    clientsData[pollFDS[j].fd].writeBuffer = clientsData[pollFDS[j].fd].buffer;
                    std::strcpy(clientsData[pollFDS[j].fd].writeBuffer, chatRoomUsers.data());
                }


            } else if (pollFDS[i].revents & POLLERR) {
                // TODO 没写这个逻辑
                //  这个逻辑是如果产生了不可避免的错误怎么办

            } else if (pollFDS[i].revents & POLLRDHUP) {
                std::string leaveUserName = clientsData[pollFDS[i].fd].getUserName();
                // 如果客户端关闭连接, 则服务器也关闭对应的连接, 并将连入到Server的Client总数减一。
                // TODO
                close(pollFDS[i].fd);
                // 在pollFDS数组中clientsCounter位置(即pollFDS记录的最后一个用户)的pollFDS的connectionFD来放到刚刚删除的这里。
                // 在纸上画一画帮助理解
                pollFDS[i] = pollFDS[clientsCounter];
                // 并且将ClientsData[clientsCounter]的数据也放到刚刚离开Server连接的那个客户端数据索引上。
                // TODO 注意: pollFDS[]与clientsData[]是一个映射的关系。clientsData[]采用了以空间换取效率。
                clientsData[pollFDS[i].fd] = clientsData[pollFDS[clientsCounter].fd];
                // 现在此时将刚刚离开的连接的pollFDS监听数组中移除了, 而且ClientData数组中保存的它数据也移除了。
                // TODO 注意: 这里是没有内存泄漏。

                // TODO
                // 这个--i我觉得没有必要。
                // 但是经过测试, 没有是不行的！
                // 不对反转了卧槽, 我又编译了一遍, 没有--i也是可以的！
                // 在下面我想通了。要加上--i。
                --i;
                --clientsCounter;
                std::fprintf(stdout, "\n%s 离开了\n", leaveUserName.data());

                // TODO 私聊:
                //  向每一个用户发送当前聊天室的成员信息。
                //  每个人
                //  不行, 不能加这些
//                std::string chatRoomUsers("当前聊天室成员为: ");
//                for (int j = 1; j <= clientsCounter; ++j) {
//                    std::fprintf(stdout, "%s ", clientsData[pollFDS[j].fd].getUserName().data());
//                    chatRoomUsers += clientsData[pollFDS[j].fd].getUserName();
//                    std::fflush(stdout);
//                }
//                std::fprintf(stdout, "\n");

#ifdef error_1_offline
                std::string chatRoomUsers("当前聊天室成员为: ");
                // TODO 私聊:
                //  向每一个人通知某个用户下线信息
                for (int j = 1; j <= clientsCounter; ++j) {

                    /*
                     * TODO 注意: 暂时取消其他客户连接的可读事件, 因为服务器要先发送数据到每一个客户端
                     * 这句代码应该写成 fds[j].events &= ~POLLIN;否则所有选项都将置位，程序虽然能正常运行，但不是作者本意。
                     */
                    // TODO https://bbs.csdn.net/topics/396849194?list=70278186
                    // TODO https://blog.csdn.net/weixin_38514703/article/details/106254010
                    // TODO https://blog.csdn.net/gatieme/article/details/50978320
                    pollFDS[j].events |= ~POLLIN;
                    // 期待数据可写这件事儿
                    pollFDS[j].events |= POLLOUT;
                    // 将当前的connectionFD的内容全部发送给其它连接到Server的Client。

                    // TODO 注意: 一样的问题, 要放在循环外
//                    std::string chatRoomUsers("当前聊天室成员为: ");
                    chatRoomUsers += clientsData[pollFDS[j].fd].getUserName();
                    clientsData[pollFDS[j].fd].writeBuffer = clientsData[pollFDS[j].fd].buffer;
//                    clientsData[pollFDS[j].fd].setWriteBuffer(const_cast<char*>(chatRoomUsers.data()));
                    std::strcpy(clientsData[pollFDS[j].fd].writeBuffer, chatRoomUsers.data());
                }
#endif

                // TODO 私聊:
                //  向每一个人通知某个用户下线信息
                std::string chatRoomUsers("当前聊天室成员为: ");
                for (int j = 1; j <= clientsCounter; ++j) {
                    chatRoomUsers += clientsData[pollFDS[j].fd].getUserName();
                }
                for (int j = 1; j <= clientsCounter; ++j) {

                    /*
                     * TODO 注意: 暂时取消其他客户连接的可读事件, 因为服务器要先发送数据到每一个客户端
                     * 这句代码应该写成 fds[j].events &= ~POLLIN;否则所有选项都将置位，程序虽然能正常运行，但不是作者本意。
                     */
                    // TODO https://bbs.csdn.net/topics/396849194?list=70278186
                    // TODO https://blog.csdn.net/weixin_38514703/article/details/106254010
                    // TODO https://blog.csdn.net/gatieme/article/details/50978320
                    pollFDS[j].events |= ~POLLIN;
                    // 期待数据可写这件事儿
                    pollFDS[j].events |= POLLOUT;
                    clientsData[pollFDS[j].fd].writeBuffer = clientsData[pollFDS[j].fd].buffer;
                    std::strcpy(clientsData[pollFDS[j].fd].writeBuffer, chatRoomUsers.data());
                }


            } else if (pollFDS[i].revents & POLLIN) {
                // 如果poll监听的某个fd有数据了, 那就读
                std::memset(clientsData[pollFDS[i].fd].buffer, '\0', Server07::getBufferSize());
                // server接受数据, 写入buffer。
                ssize_t retVal = recv(pollFDS[i].fd, clientsData[pollFDS[i].fd].buffer, Server07::getBufferSize() - 1,
                                      0);
                // TODO 这里的from %d 最后改为具体的名字。(跟私聊功能实现有关)
                //  而且要对这条信息进行判断, 如果是发送给某个人是不需要执行后面的语句的。
                inet_ntop(AF_INET, &clientsData[pollFDS[i].fd].getSockAddrIn().sin_addr,
                          clientsData[pollFDS[i].fd].ip, INET_ADDRSTRLEN);
                std::fprintf(stdout,
                             "\n得到 %zd 字节的用户数据, 数据: %s 来自 %s:%s\n",
                             retVal,
                             clientsData[pollFDS[i].fd].buffer,
                             clientsData[pollFDS[i].fd].ip,
                             clientsData[pollFDS[i].fd].getUserName().data());
                if (retVal < 0) {
                    // 如果读操作出错, 则关闭连接
                    // TODO 这里和上面的client连接断开是一样的操作
                    //  不过这个errno现在不懂。
                    if (errno != EAGAIN) {
                        close(pollFDS[i].fd);
                        clientsData[pollFDS[i].fd] = clientsData[pollFDS[clientsCounter].fd];
                        pollFDS[i] = pollFDS[clientsCounter];
                        // TODO
                        // 同样, 我觉得--i不是很有必要
                        // 不过就在刚刚我想通了。--i;后然后在for中又++i, 此时poll轮询当前的connectionFD。
                        // 如果没有--i也是可以的, 就是下次轮询到这个connectionFD时要等一轮。
                        // 我想通了现在。所以还是加上吧。
                        --i;
                        --clientsCounter;
                    }
                } else if (retVal == 0) {
                    // 没有对这个逻辑的描述
                    // 其实是可以删掉的。
                } else {
                    // TODO 私聊:


                    // 如何Server读到了某个Client的数据, 那么通过Server中连接其它Client的connectionFD准备写数据
                    // 这里的condition中的=号是一定要有的。因为要通知所有连接Server的客户端。
                    for (int j = 1; j <= clientsCounter; ++j) {
                        // 除了当前的connectionFD无需通知外
                        if (pollFDS[j].fd == pollFDS[i].fd) {
                            // TODO 注意: 这里是要添加的！！！！, 因为之后的发送数据的逻辑, 需要判断写的缓冲区是否为空。
//                            clientsData[pollFDS[j].fd].setWriteBuffer(nullptr);
//                            std::strcpy(clientsData[pollFDS[j].fd].writeBuffer, nullptr);
                            clientsData[pollFDS[j].fd].writeBuffer = nullptr;
                            continue;
                        }

                        /*
                         * TODO 注意: 暂时取消其他客户连接的可读事件, 因为服务器要先发送数据到每一个客户端
                         * 这句代码应该写成 fds[j].events &= ~POLLIN;否则所有选项都将置位，程序虽然能正常运行，但不是作者本意。
                         */
                        // TODO https://bbs.csdn.net/topics/396849194?list=70278186
                        // TODO https://blog.csdn.net/weixin_38514703/article/details/106254010
                        // TODO https://blog.csdn.net/gatieme/article/details/50978320
                        pollFDS[j].events |= ~POLLIN;
                        // 期待数据可写这件事儿
                        pollFDS[j].events |= POLLOUT;
                        // 将当前的connectionFD的内容全部发送给其它连接到Server的Client。

//                        clientsData[pollFDS[j].fd].setWriteBuffer(clientsData[pollFDS[i].fd].getBuffer());
                        // TODO 注意: 这里是对上一句的改进, 因为想要实现私聊功能。
                        String msg(clientsData[pollFDS[i].fd].getUserName() + ":" +
                                   clientsData[pollFDS[i].fd].getBuffer());
                        clientsData[pollFDS[j].fd].writeBuffer = clientsData[pollFDS[j].fd].buffer;
//                        clientsData[pollFDS[j].fd].setWriteBuffer(const_cast<char*>(msg.data()));
                        std::strcpy(clientsData[pollFDS[j].fd].writeBuffer, msg.data());
                    }
                }

            } else if (pollFDS[i].revents & POLLOUT) {
                // TODO 这个逻辑分支不知道在干什么
                // 通过测试我知道了这个逻辑分支在干什么: 这个逻辑分支将上一个逻辑分支的客户端发来的内容一个个发送到其它的客户端上。
                int connectFD = pollFDS[i].fd;
                if (!clientsData[connectFD].writeBuffer) {
//                if (*clientsData[connectFD].writeBuffer == '\0') {
                    continue;
                }
                // 通知连接到这个端口的所有客户端发送数据
//                ssize_t retVal = send(connectFD, clientsData[connectFD].getWriteBuffer(),
//                                      std::strlen(clientsData[connectFD].getWriteBuffer()), 0);
//                ssize_t retVal = send(connectFD, clientsData[connectFD].writeBuffer,
//                                      std::strlen(clientsData[connectFD].writeBuffer), 0);
                ssize_t retVal = send(connectFD, clientsData[connectFD].buffer,
                                      std::strlen(clientsData[connectFD].buffer), 0);

                // TODO 注意: 找到(发送数据的客户端的服务器的)connectedFD的fd, 因为之后要 来自客户端的需求
                //  这样的话要先在ClientData添加一个字段, 这个字段与Server的connectedFD进行配对
                int dataFrom{-1};
                for (int j = 1; j <= clientsCounter; ++j) {
                    if (clientsData[pollFDS[j].fd].writeBuffer) {
//                    if (*clientsData[pollFDS[j].fd].writeBuffer == '\0') {
                        dataFrom = pollFDS[j].fd;
                    }
                }
//                std::fprintf(stdout, "\n成功向其它所有用户发送: %zd 字节的数据, 数据: %s , 来自用户: %s\n",
//                             retVal,
//                             clientsData[connectFD].getWriteBuffer(),
//                             clientsData[dataFrom].getUserName().data());
                std::fprintf(stdout, "\n成功向其它所有用户发送: %zd 字节的数据, 数据: (%s) , 来自 %s:%s\n",
                             retVal,
                             clientsData[connectFD].buffer,
                             clientsData[dataFrom].getIp(),
                             clientsData[dataFrom].getUserName().data());
                std::fflush(stdout);

                // TODO 注意: 这一句加不加都可以。但是加上是良好的编程习惯。
                //  要加, 因为其它一些逻辑要对这个writeBuffer是否为nullptr进行判断。
//                clientsData[connectFD].setWriteBuffer(nullptr);
//                std::memset(clientsData[connectFD].writeBuffer, '\0', sizeof(clientsData[connectFD].writeBuffer));
                clientsData[connectFD].writeBuffer = nullptr;
                // TODO
                // 写完数据后需要重新注册pollFDS[i]上的可读事件
                // 监听是否有 非写 或者 可读
                pollFDS[i].events |= ~POLLOUT;
                pollFDS[i].events |= POLLIN;
            }
        }
    }

    delete[] clientsData;
    close(listenFD);

    return 0;
}